﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ChoWorkflowServer.Model;
using Cinchoo.Core.ETL;
using Cinchoo.Core.Diagnostics;
using Cinchoo.Core;
using Cinchoo.Core.Services;

public enum ChoTaskScheduleType { Daily = 1, Weekly, Monthly, Once }

namespace ChoWorkflowServer
{
    public class ChoWorkflowScheduler : IDisposable, IChoWorkflowScheduler
    {
        private readonly IChoDataModel _dataModel = ChoDataModelFactory.New();
        private readonly ChoJobStore _jobStore = new ChoJobStore();
        private readonly ChoETLJobScheduler _scheduler;
        private readonly ChoDependencyWorkflowScheduler _dependencyWorkflowScheduler;

        public event EventHandler<ChoTaskSchedularEventArgs> JobScheduled = delegate { };
        public event EventHandler<ChoTaskSchedularEventArgs> JobActivate = delegate { };
        public event EventHandler<ChoTaskSchedularEventArgs> JobFired = delegate { };

        public ChoWorkflowScheduler()
        {
            _dependencyWorkflowScheduler = new ChoDependencyWorkflowScheduler();
            _scheduler = new ChoETLJobScheduler(_jobStore, 100);
            _scheduler.JobScheduled += new EventHandler<ChoTaskSchedularEventArgs>(OnJobScheduled);
            _scheduler.JobActivate += new EventHandler<ChoTaskSchedularEventArgs>(OnJobActivate);
            _scheduler.JobFired += new EventHandler<ChoTaskSchedularEventArgs>(OnJobFired);
        }

        public void ScheduleWorkflows(ChoTasksDataSet.CHO_TASKSDataTable _tasksDataTable)
        {
            if (_tasksDataTable.Count > 0)
            {
                foreach (ChoTasksDataSet.CHO_TASKSRow row in _tasksDataTable)
                {
                    //if (!row.IsTASK_STATUSNull() && (ChoETLWorkflowStatus)row.TASK_STATUS == ChoETLWorkflowStatus.INACTIVE)
                    //    continue;

                    if (row.IsSCHEDULE_NAMENull()) continue;

                    string errMsg = null;
                    string msg = null;
                    DateTimeOffset? nextRuntime = UpdateJobToScheduler(row.TASK_ID, row.TASK_NAME, row.SCHEDULE_NAME, row.WORKFLOW_NAME, (ChoETLWorkflowStatus)row.TASK_STATUS, out errMsg, out msg);

                    row.NEXT_RUN_TIME = msg.IsNullOrWhiteSpace() ? FormatNextRunTimeMessage(nextRuntime, errMsg) : msg; //FormatNextRunTimeMessage(nextRuntime, errMsg);

                    //AddJobToScheduler(row.TASK_ID, row.TASK_NAME, row.SCHEDULE_NAME, row.WORKFLOW_NAME);
                }
            }
        }

        public void FireDependentTasks(int batchId, string taskName, string scheduleName, string user, string originHostName)
        {
            _dependencyWorkflowScheduler.FireDependentTasks(batchId, taskName, scheduleName, user, originHostName);
        }

        public void CancelDependentTask(string taskName, string scheduleName)
        {
            _dependencyWorkflowScheduler.CancelDependentTask(taskName, scheduleName);
        }

        public void FireActivateDependentTasks(int batchId, string taskName, string scheduleName, string user, string originHostName)
        {
            _dependencyWorkflowScheduler.FireActivateDependentTasks(batchId, taskName, scheduleName, user, originHostName);
        }

        public void ResetCalendar(string calName)
        {
            if (calName.IsNullOrWhiteSpace()) return;

            _jobStore.ResetCalendar(calName);
        }

        [ChoStreamProfile("WorkflowSchedules...", Name = "wfSchedules", OuterProfileName = null)]
        private void OnJobScheduled(object sender, ChoTaskSchedularEventArgs e)
        {
            ChoProfile.WriteLine("SCHEDULED: " + e.JobName + " " + (e.FireTimeUtc != null ? TimeZoneInfo.ConvertTime(e.FireTimeUtc.Value, TimeZoneInfo.Local).ToString() : String.Empty));
            JobScheduled(sender, e);
        }

        [ChoStreamProfile("WorkflowSchedules...", Name = "wfSchedules", OuterProfileName = null)]
        private void OnJobActivate(object sender, ChoTaskSchedularEventArgs e)
        {
            ChoProfile.WriteLine("ACTIVATE: " + e.JobName + " " + (e.FireTimeUtc != null ? TimeZoneInfo.ConvertTime(e.FireTimeUtc.Value, TimeZoneInfo.Local).ToString() : String.Empty));
            JobActivate(sender, e);
        }

        [ChoStreamProfile("WorkflowSchedules...", Name = "wfSchedules", OuterProfileName = null)]
        private void OnJobFired(object sender, ChoTaskSchedularEventArgs e)
        {
            ChoProfile.WriteLine("FIRE: " + e.JobName + " " + (e.FireTimeUtc != null ? TimeZoneInfo.ConvertTime(e.FireTimeUtc.Value, TimeZoneInfo.Local).ToString() : String.Empty));
            JobFired(sender, e);
        }

        internal DateTimeOffset? AddJobToScheduler(int taskId, string taskName, string scheduleName, string wfName, ChoETLWorkflowStatus status, out string errMsg, out string msg)
        {
            errMsg = null;
            msg = null;

            if (scheduleName.IsNullOrWhiteSpace() || wfName.IsNullOrWhiteSpace())
                return null;
            if (status == ChoETLWorkflowStatus.INACTIVE)
                return null;

            DateTimeOffset? nextRun = null;
            lock (_scheduler.SyncRoot)
            {
                try
                {
                    ChoTasksDataSet.CHO_TASK_SCHEDULESDataTable tbl = _dataModel.GetTaskSchedules(taskId, scheduleName);
                    if (tbl.Rows.Count > 1)
                        throw new ApplicationException("Found duplicate task schedule definitions");

                    foreach (ChoTasksDataSet.CHO_TASK_SCHEDULESRow scheduleRow in tbl)
                    {
                        if (!scheduleRow.ENABLED)
                            throw new ApplicationException("Task schedule is NOT enabled.");

                        string name = ChoJobNameHelper.ToJobName(taskName, scheduleRow.SCHEDULE_NAME);
                        
                        if (!scheduleRow.IsDEPENDENCIESNull() && !scheduleRow.DEPENDENCIES.IsNullOrWhiteSpace())
                        {
                            //_dependencyWorkflowScheduler.AddJobToScheduler(name, scheduleRow);
                            msg = scheduleRow.DEPENDENCIES;
                            return null;
                        }

                        if (_scheduler.IsJobScheduled(name))
                            throw new ApplicationException("Duplicate job found in the scheduler.");

                        ChoTaskScheduleType taskScheduleType = (ChoTaskScheduleType)scheduleRow.SCHEDULE_TYPE_ID;
                        DateTime startDate = scheduleRow.START_DATE;
                        if (startDate < DateTime.Today)
                            startDate = DateTime.Today;

                        DateTime localStartDateTime = new DateTime(startDate.Year, startDate.Month, startDate.Day, scheduleRow.START_TIME.Hour, scheduleRow.START_TIME.Minute, scheduleRow.START_TIME.Second);
                        //if (localStartDateTime < DateTime.Now)
                        //    localStartDateTime = DateTime.Now;

                        DateTimeOffset startTimeUtc = new DateTimeOffset(TimeZoneInfo.ConvertTime(localStartDateTime, TimeZoneInfo.Local, TimeZoneInfo.Utc), TimeSpan.Zero);
                        DateTimeOffset? endTimeUtc = null;

                        if (!scheduleRow.IsEND_DATENull() && scheduleRow.END_DATE != ChoDbDateTime.MinValue && !scheduleRow.IsEND_TIMENull())
                        {
                            DateTime localEndDateTime;
                            localEndDateTime = new DateTime(scheduleRow.END_DATE.Year, scheduleRow.END_DATE.Month, scheduleRow.END_DATE.Day, scheduleRow.END_TIME.Hour, scheduleRow.END_TIME.Minute, scheduleRow.END_TIME.Second);

                            if (localEndDateTime > DateTime.Now)
                                throw new ApplicationException("Task can't be scheduled as end date passed the current time.");

                            endTimeUtc = new DateTimeOffset(TimeZoneInfo.ConvertTime(localEndDateTime, TimeZoneInfo.Local, TimeZoneInfo.Utc), TimeSpan.Zero);
                        }
                        //else if (scheduleRow.END_DATE != ChoDbDateTime.MinValue && scheduleRow.IsEND_TIMENull())
                        //    endTimeUtc = new DateTimeOffset(scheduleRow.END_DATE.Year, scheduleRow.END_DATE.Month, scheduleRow.END_DATE.Day, 23, 59, 59, TimeSpan.Zero);

                        ChoETLTrigger trigger = null;

                        switch (taskScheduleType)
                        {
                            case ChoTaskScheduleType.Daily:
                                trigger = new ChoDailyCalendarIntervalTrigger(name, startTimeUtc, endTimeUtc);
                                break;
                            case ChoTaskScheduleType.Monthly:
                                trigger = new ChoMonthlyCalendarIntervalTrigger(name, startTimeUtc, endTimeUtc);
                                break;
                            case ChoTaskScheduleType.Weekly:
                                trigger = new ChoWeeklyCalendarIntervalTrigger(name, startTimeUtc, endTimeUtc);
                                break;
                            case ChoTaskScheduleType.Once:
                                endTimeUtc = startTimeUtc.AddDays(1);
                                trigger = new ChoETLCalendarIntervalTrigger(name, startTimeUtc, endTimeUtc, ChoETLIntervalUnit.Day);
                                break;
                        }

                        if (trigger == null)
                            throw new ApplicationException("Scheduler trigger not found.");

                        if (!scheduleRow.IsT1Null())
                            trigger.T1Days = scheduleRow.T1;
                        trigger.Tag = scheduleRow;

                        string holidayName = null;

                        if (!scheduleRow.IsHOLIDAY_CAL_NAMENull())
                            holidayName = scheduleRow.HOLIDAY_CAL_NAME;

                        ChoETLJobDetail jd = new ChoETLJobDetail(name,
                            holidayName)
                            {
                                Trigger = trigger,
                                BatchId = ChoRandom.NextRandom(),
                            Tag = new Tuple<string, string, string>(taskName, scheduleRow.SCHEDULE_NAME, wfName) };

                        nextRun = _scheduler.ScheduleJob(jd);
                    }
                }
                catch (Exception ex)
                {
                    errMsg = ex.Message;
                    ChoTrace.Error(ex);
                }
            }

            return nextRun;
        }

        internal string FormatNextRunTimeMessage(DateTimeOffset? nextRuntime, string errMsg)
        {
            if (!errMsg.IsNullOrWhiteSpace())
                return errMsg;
            else if (nextRuntime == null)
                return DateTime.MinValue.ToString("G");
            else
                return nextRuntime.Value.DateTime.ToString("G");
        }

        internal DateTimeOffset? UpdateJobToScheduler(int taskId, string taskName, string scheduleName, string wfName, ChoETLWorkflowStatus status, out string errMsg, out string msg)
        {
            lock (_scheduler.SyncRoot)
            {
                RemoveJobFromScheduler(taskId, taskName, scheduleName);

                return AddJobToScheduler(taskId, taskName, scheduleName, wfName, status, out errMsg, out msg);
            }
        }

        internal void RemoveJobFromScheduler(int taskId, string taskName, string scheduleName)
        {
            if (scheduleName.IsNullOrWhiteSpace())
                return;

            lock (_scheduler.SyncRoot)
            {
                try
                {
                    string name = ChoJobNameHelper.ToJobName(taskName, scheduleName);
                    _scheduler.UnscheduleJob(name);
                }
                catch (Exception ex)
                {
                    ChoTrace.Error(ex);
                }
            }
        }

        public void Dispose()
        {
            _scheduler.Dispose();
        }
    }
}
